﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Data.Linq.Mapping;
using System.Linq.Expressions;

namespace OneSystems.Data.Helpers
{
    /// <summary>
    /// Internal utility class used to remove the need to 
    /// maintain matching properties for database tables.
    /// Used to read and update columns based on the ColumnAttribute 
    /// class defined by the System.Data.Linq namespace
    /// </summary>
    internal class EntityReflector
    {
        // cache of primary key property get methods
        private static Dictionary<Int32, PropertyInfo> m_PrimaryKeys = new Dictionary<Int32, PropertyInfo>();
        // thread-safe lock for updating m_PrimaryKeys
        private static Object m_SyncLock = new Object();

        /// <summary>
        /// Gets value of the entity's primary key
        /// </summary>
        /// <param name="entity">The entity for which the value is needed</param>
        /// <returns>The Int32 value of the primary key or default(Int32)</returns>
        public static Int32 GetPrimaryKeyValue(Object entity)
        {
            PropertyInfo primaryKey = GetPrimaryKeyProperty(entity.GetType());

            if (primaryKey == null)
                return default(Int32);

            return (Int32)primaryKey.GetValue(entity, null);
        }

        /// <summary>
        /// Sets value of the entity's primary key
        /// </summary>
        /// <param name="entity">The entity for which the value will be set</param>
        public static void SetPrimaryKeyValue(Object entity, Int32 value)
        {
            PropertyInfo primaryKey = GetPrimaryKeyProperty(entity.GetType());

            if (primaryKey == null)
                return;

            primaryKey.SetValue(entity, value, null);
        }

        #region Private utility methods

        /// <summary>
        /// Gets the PropertyInfo object representing the get method
        /// of the primary key property
        /// </summary>
        /// <param name="type">the Type of the entity which has a primary key</param>
        /// <returns>The PropertyInfo object representing the get method</returns>
        public static PropertyInfo GetPrimaryKeyProperty(Type type)
        {
            PropertyInfo primaryKey = null;

            // get the hash once since we'll need it multiple times
            Int32 hash = type.GetHashCode();

            // check to see if the property has already been found 
            if (m_PrimaryKeys.ContainsKey(hash))
            {
                // set local variable to property get method
                primaryKey = m_PrimaryKeys[hash];
            }
            else
            {
                // search the public instance get properties
                PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty
                    | BindingFlags.DeclaredOnly);

                // look for the first match
                foreach (PropertyInfo property in properties)
                {
                    // get the ColumnAttribute (we don't NEED to check the inheritance tree, so until we do we'll exlude it for better performance)
                    object[] columnAttributes = property.GetCustomAttributes(typeof(ColumnAttribute), false);

                    // if the property doesn't have a ColumnAttribute, skip it
                    if (columnAttributes.Length == 0)
                        continue;

                    // check for IsPrimaryKey flag
                    if (((ColumnAttribute)columnAttributes[0]).IsPrimaryKey)
                    {
                        // get the property and exit loop
                        primaryKey = property;
                        break;
                    }
                }

                // since we're here check to see if we need to add the property to cache
                if (!m_PrimaryKeys.ContainsKey(hash))
                {
                    // block other updating threads
                    lock (m_SyncLock)
                    {
                        // check again to prevent race condition
                        if (!m_PrimaryKeys.ContainsKey(hash))
                            m_PrimaryKeys.Add(hash, primaryKey);
                    }
                }
            }

            return primaryKey;
        }

        /// <summary>
        /// Builds a predicate expression to match for the primary key of the entity
        /// </summary>
        /// <param name="id">The primary key value to match</param>
        /// <returns>The lambda predicate expression (e => e.ID == id)</returns>
        public static Expression<Func<T, Boolean>> BuildPrimaryKeyPredicate<T>(Int32 id) where T : class
        {
            PropertyInfo primaryKey = GetPrimaryKeyProperty(typeof(T));

            // build predicate expressions
            ParameterExpression parameter = Expression.Parameter(typeof(T), "e");	// e =>
            Expression left = Expression.Property(parameter, primaryKey);			// e => e.ID
            Expression right = Expression.Constant(id);								// id
            Expression body = Expression.Equal(left, right);						// e => e.ID == id

            // create predicate
            Expression<Func<T, Boolean>> predicate = Expression.Lambda<Func<T, Boolean>>(
                // expression body
                body,
                // expression parameter
                new ParameterExpression[] { parameter }
            );

            return predicate;
        }

        #endregion
    }
}
